package org.melanesia.sql;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.melanesia.beans.Mapper;
import org.melanesia.beans.PropertyMapper;
import org.melanesia.converters.JavaToJavaConverter;
import org.melanesia.converters.JavaToJdbcConverter;
import org.melanesia.sql.exceptions.SQLExecutionException;
import org.melanesia.sql.exceptions.SQLMappingException;


public class Query extends AbstractSQL {
    
	private final static Logger logger = Logger.getLogger(Query.class.getName());
	
	// FIXME: this should be in some configuration file
	private final static int DEFAULT_FETCH_SIZE = 1000;
	
    private PropertyMapper propertyMapper = new PropertyMapper();
    
    private Connection connection;
    private String sql;
    QueryMetaData queryMetaData = new QueryMetaData();
    
    private Map<String, Class<?>> beanClasses = new HashMap<String, Class<?>>();
    
    Query(Connection connection, Mapper mapper, String sql) {
        super(mapper);
        this.connection = connection;
        this.sql = sql;
    }
    
    public Query setParameter(String name, Object value) {
        queryMetaData.setInputParameter(name, value);
        return this;
    }

    public Query setParameter(String name, Object value, JavaToJdbcConverter converter) {
        queryMetaData.setInputParameter(name, value, converter);
        return this;
    }
    
    public Query setColumnConverter(String columnLabel, JavaToJavaConverter converter) {
        queryMetaData.setColumnConverter(columnLabel, converter);
        return this;
    }

    /**
     * Sets class for a particular bean property  
     * This is used in situations, where we have a compound object like for example:
     * 
     * <code>
     * class A {
     *   private B b;
     *   private String propertyC;
     *   private String propertyD;
     *   private Integer propertyE;
     * }
     * 
     * class B {
     *   private String propertyF;
     *   private String propertyG;
     * }
     * </code>
     * 
     * We can then write SQL statament to select data for object A like this:
     * 
     * <code>
     * SELECT
     *   ... AS "propertyC",
     *   ... AS "propertyD",
     *   ... AS "propertyE",
     *   ... AS "b.propertyF"
     *   ... AS "b.propertyG"
     * FROM ...
     * </code>
     * 
     * Using this notatnion SQLMapper will try to create an instance of B and then set its properties: propertyF and propertyG.
     * This notatnion can go futher - for example "b.c.d.propertyX".
     * 
     * The problem with above is that B can be an abstract class, an interface or a superclass for some other class, a programmer wants to instantiate.
     * This problem can be solved by using this method and passing Class reference to be instantiated for the property. 
     * 
     * @param bean           Name of the beanProperty used in SQL statement.
     *                       For example: "b", "b.c", "b.c.d"
     * @param beanClass      Class to be used when instantiating the bean.
     * @return               This query.
     */
    public Query setPropertyBeanClass(String bean, Class<?> beanClass) {
        beanClasses.put(bean, beanClass);
        return this;
    }
    
    public Query setPropertyMapper(PropertyMapper propertyMapper) {
        this.propertyMapper = propertyMapper;
        return this;
    }
    
    public <T> List<T> execute(Class<T> returnedBeanClass) {
    	return executeInternal(returnedBeanClass, 0);
    }
    
    public <T> T executeSingle(Class<T> returnedBeanClass) {
    	List<T> list = executeInternal(returnedBeanClass, 1);
    	if (!list.isEmpty()) {
    		return list.get(0);
    	} else {
    		return null;
    	}
    }
    
    private <T> List<T> executeInternal(Class<T> returnedBeanClass, int maxFetchSize) {

        ArrayList<T> result = new ArrayList<T>();
        
        CallableStatement stmt = null;
        ResultSet rs = null;
        ResultSetMetaData rsmd = null;
        
        try {

        	String sqlId = String.valueOf(sql.hashCode());
        	logger.info("[QUERY:" + sqlId + "]: Executing query\n" + sql);
        	
            stmt = connection.prepareCall(sql);
            
            for (String parameterName : queryMetaData.getInputParameterNames()) {
            	
            	InputParameter parameter = queryMetaData.getInputParameter(parameterName);
                setInputParameter(stmt, parameter);
                
                logger.info("[QUERY:" + sqlId + "]: Bound parameter " + parameter.getName() + " = " + parameter.getValue());
            }
            
            if (maxFetchSize > 0) {
            	stmt.setFetchSize(maxFetchSize);
            } else {
            	stmt.setFetchSize(DEFAULT_FETCH_SIZE);
            }
            
            long sqlTimeStart = System.currentTimeMillis();
            rs = stmt.executeQuery();
            rsmd = rs.getMetaData();
            long sqlTime = System.currentTimeMillis() - sqlTimeStart;
            
            long mappingTimeStart = System.currentTimeMillis();
            int cnt = 0;
            while (rs.next() && (maxFetchSize <= 0 || maxFetchSize > cnt)) {
                
            	// FIXME: Will this work when Map.class is passed?
                T bean = returnedBeanClass.newInstance();
             
                for (int i = 1, size = rsmd.getColumnCount(); i <= size; i++) {

                    String columnLabel = rsmd.getColumnLabel(i);
                    Object columnValue = rs.getObject(columnLabel);
                    
                    propertyMapper.setProperty(bean, columnLabel, columnValue, mapper);
                }
                
                result.add(bean);
                ++cnt;
            }
            long mappingTime = System.currentTimeMillis() - mappingTimeStart;
            
            logger.info("[QUERY:" + sqlId + "]: DB: " + sqlTime + "ms, FETCH&MAPPING: " + mappingTime + "ms, ROWS: " + cnt);
            
            return result;
            
        } catch (SQLException ex) {
            throw new SQLExecutionException(ex);
        } catch (InstantiationException ex) {
            throw new SQLMappingException(ex);
        } catch (IllegalAccessException ex) {
            throw new SQLMappingException(ex);
        } finally {
            try {rs.close();} catch (Throwable t) {}
            try {stmt.close();} catch (Throwable t) {}
        }
    }

    

    
    
}
